Skip to content

fix(h2): escape SQL identifiers and literals in metadata/DDL paths (#1914) - #2177

Merged
openai0229 merged 6 commits into
OtterMind:mainfrom
HandSonic:fix/sqli-h2
Jul 29, 2026
Merged

fix(h2): escape SQL identifiers and literals in metadata/DDL paths (#1914)#2177
openai0229 merged 6 commits into
OtterMind:mainfrom
HandSonic:fix/sqli-h2

Conversation

@HandSonic

@HandSonic HandSonic commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Related issue

Part of #1914. Depends on the shared identifier contract merged in #2234.

Summary

  • Rebased the H2 hardening onto the shared ISQLIdentifierProcessor#quoteIdentifierAlways contract.
  • Added H2IdentifierProcessor with H2-aware conditional quoting, unconditional DDL quoting, embedded double-quote round trips, and raw string-literal escaping.
  • Routed H2 metadata, manager, and builder SQL construction through that processor instead of a parallel escaping helper.
  • Added strict guards for metadata type names and default expressions so structural breakout input is rejected while legal H2 literals, temporal defaults, and sequence defaults keep their semantics.
  • Rendered metadata type parameters from JDBC DATA_TYPE, precision, and scale instead of treating display width as a type argument. This avoids invalid exports such as BIGINT(64) and TIMESTAMP(26).
  • Added focused builder/manager/metadata coverage plus a real in-memory H2 export-and-replay test.

Affected surfaces

  • Frontend / Web
  • Backend / API / Storage
  • Database plugin / Driver
  • JCEF / Desktop packaging
  • CI / Build / Release
  • Documentation only

Verification

  • Commands and results: mvn -B -f chat2db-community-server/pom.xml -pl chat2db-community-plugins/chat2db-community-h2 -am -Dmaven.test.skip=false -DskipTests=false clean test completed with BUILD SUCCESS; H2 tests: 30 run, 0 failures, 0 errors, 0 skipped. git diff --check also passed.
  • Manual verification: N/A. The integration test creates a source H2 database, exports DDL through H2Meta, executes it in a second H2 database, inserts default values, and verifies sequence, timestamp, and string defaults.
  • UI evidence: N/A

Risk and compatibility

  • Public API or stored data: N/A. No public API or stored-data format changes.
  • Database or driver compatibility: H2-generated identifiers are now consistently delimited. JDBC metadata sizes are rendered according to the reported SQL type so exported DDL remains executable.
  • Network, privacy, or security: No network or privacy changes. This hardens second-order SQL construction paths for metadata-controlled names, comments, type declarations, and defaults.
  • Community / Local / Pro boundary: Community H2 plugin only; downstream products receive the behavior through the shared Community plugin.
  • Backward compatibility: Ordinary identifiers retain their meaning. Case-sensitive and delimiter-containing identifiers now round-trip correctly. Unsafe metadata expressions fail closed instead of being emitted as SQL.

Reviewer map

  • Start here: H2IdentifierProcessor, H2SqlGuards, and H2Meta; then review the escaped call sites in H2SqlBuilder and H2DBManager.
  • Failure condition: conditional and always-quote behavior diverges from feat(spi): add quoteIdentifierAlways to ISQLIdentifierProcessor #2234, quote/remove no longer round-trips, structural input escapes its SQL position, or valid H2 metadata defaults cannot be replayed.
  • Rollback or disable path: Revert this PR.

Contributor declaration

  • I linked the Issue that defines this change.
  • I tested the affected behavior and reported the actual results above.
  • I did not include credentials, private data, or generated build output.
  • I disclosed substantial AI assistance below, or this PR contains no substantial AI-generated code.

AI assistance: The implementation, adversarial test cases, compatibility review, and PR description were produced with AI coding assistance and maintainer review.

@openai0229 openai0229 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for hardening these second-order SQL injection paths. The escaping behavior added here is needed, but H2SqlEscapes creates a second dialect-escaping abstraction. The Community SPI already defines ISQLIdentifierProcessor as the owner of both identifier quoting (quoteIdentifier) and SQL string-literal escaping (escapeString). H2 currently inherits DefaultSQLIdentifierProcessor through DefaultMetaService.

Please implement an H2-specific identifier processor (or strengthen the appropriate shared processor), return it from H2Meta#getSQLIdentifierProcessor(), and reuse that processor from H2Meta, H2DBManager, and H2SqlBuilder instead of routing these call sites through H2SqlEscapes.

Simply switching to the current DefaultSQLIdentifierProcessor is not sufficient: its quoteIdentifier only wraps invalid names and does not double embedded double quotes, while its escapeString preserves adjacent quote pairs instead of encoding every quote in a raw value. The processor implementation therefore needs focused coverage for embedded identifier quotes, already quoted identifiers, consecutive literal quotes, and case-sensitive H2 metadata names. Identifier quoting and string-literal escaping should remain separate processor methods.

This keeps one reusable dialect contract and prevents future H2 SQL-building call sites from bypassing the hardening.

@openai0229

openai0229 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Would you be willing to take this opportunity to improve this class of architectural issue more broadly, rather than limiting the change to the seven H2 call sites in this diff?

It looks likely that identifier quoting and SQL string-literal escaping have drifted away from ISQLIdentifierProcessor in multiple places. If you are open to it, could you audit the relevant plugin SQL-construction paths and refactor the non-conforming logic to use the processor abstraction consistently, strengthening the processor implementations where needed? The goal would be to eliminate parallel per-plugin escapers and make the existing architecture the single reusable path.

This is a broader improvement request, so please let us know whether you would be willing to take it on as part of this work, or as focused follow-up work if the scope is too large for this PR.

@HandSonic

Copy link
Copy Markdown
Contributor Author

Agreed — thank you for the direction. I've reworked this branch onto the SPI contract:

  • New H2IdentifierProcessor extends DefaultSQLIdentifierProcessor: quoteIdentifier (double-quote doubling, strips one outer pair) and escapeString (single-quote doubling)
  • H2Meta now overrides getSQLIdentifierProcessor(); all metadata call sites use it
  • Builder/manager call sites use H2IdentifierProcessor.INSTANCE
  • Non-escapable validation (metadata-reported type names, column default expressions) moved to H2SqlGuards, clearly separated from escaping
  • H2SqlEscapes removed; tests migrated (27 green)

I will roll the same pattern out to the sibling PRs (#2172-#2176 and #2194-#2206): each plugin gets its escaping consolidated into its own ISQLIdentifierProcessor implementation (strengthening the existing processor where one already exists, e.g. mysql/oracle/sqlserver), with getSQLIdentifierProcessor() overridden on the MetaData side. The broader audit of escaping drift across plugins can follow as a separate pass once these branches share the same shape.

@HandSonic

Copy link
Copy Markdown
Contributor Author

Rollout complete: all 19 sibling PRs (#2172-#2176, #2194-#2206) now share the same shape as this branch — escaping consolidated into each plugin's ISQLIdentifierProcessor implementation (strengthened existing processors where present), getSQLIdentifierProcessor() overridden on the MetaData side, non-escapable validation in per-plugin SqlGuards.

One extra finding from our own adversarial pass during the rollout, worth flagging: making quoteIdentifier unconditionally quote broke SPI consumers (GenericSqlCompletionEngine relies on conditional-quote semantics + null passthrough). The final contract is therefore two-track: quoteIdentifier stays conditional (null->null, valid plain identifiers unquoted, fold-aware for PG) and a plugin-level quoteIdentifierAlways serves DDL-generation sites. Backtick processors override removeIdentifierQuote/isQuoteIdentifier accordingly. All module test suites green (incl. mysql 631 tests).

@openai0229

Copy link
Copy Markdown
Contributor

Thanks for completing the rollout. I reviewed the current heads of all 19 PRs. Moving the escaping implementations into plugin processors is the right direction, but the rollout is not merge-ready yet.

The main common blocker is that identifier quote/remove round-trip is still broken across the 18 relational branches. For example, a raw identifier such as A"B is quoted as "A""B", but removeIdentifierQuote returns AB instead of A"B. The backtick and SQL Server bracket implementations have the same issue. This is observable through GenericSqlCompletionEngine, which removes quotes before metadata lookup.

There are also contract and dialect regressions in the current heads:

  • quoteIdentifierAlways remains a concrete plugin API rather than part of ISQLIdentifierProcessor, while several production paths still use content-only escapeIdentifier helpers plus caller-owned delimiters.
  • H2 and XuguDB do not implement the stated conditional/null-preserving/always-quote split.
  • KingBase, DM, Oscar, Redshift, Snowflake, and OceanBase Oracle regress existing case-folding or reserved-word behavior.
  • Several SqlGuards either accept structural SQL such as added constraints/columns or convert legal default expressions into string literals.
  • MongoDB correctly needs a non-SQL path, but its current allowlist accepts names such as 1users and my-field that produce invalid JavaScript in db.%s and unquoted object-key positions.

Please add a shared processor conformance test class and run every relational processor through it. At minimum it should cover:

  • null and blank passthrough;
  • conditional quote versus always-quote semantics;
  • dialect reserved words and case folding;
  • removeIdentifierQuote(quoteIdentifierAlways(raw)) == raw for embedded ", backtick, and ] delimiters;
  • already quoted input normalization;
  • raw string escaping, including consecutive quote characters and null;
  • an integration case through the shared completion/metadata consumer.

Please also add plugin-level SQL/DDL semantic tests for legal default expressions and structural breakout inputs. Where an embedded or test-container database is available, execute the generated DDL rather than only asserting string fragments. SQLite should prove that a type such as TEXT, injected INTEGER cannot create another column; H2/PostgreSQL/Oracle-family tests should prove that legal function or typed defaults retain their original semantics. MongoDB should use syntax-aware command tests or a structured builder so valid database, collection, and field names are not conflated.

The shared SPI contract and conformance suite should be fixed first, then the sibling branches should be rebased onto it. Otherwise the same contract bug has to be corrected independently in every PR.

@openai0229

Copy link
Copy Markdown
Contributor

To make the next step concrete, please pause the per-dialect rollout for now. PR #2234 is already introducing quoteIdentifierAlways into ISQLIdentifierProcessor, so the shared contract should be settled there (or in one equivalent foundation PR) before updating 19 branches independently.

The foundation should include the shared conformance suite described above, especially the quote/remove round-trip and conditional-versus-always-quote semantics. Once that lands, please rebase H2 and MySQL first as reference implementations and add executable DDL/default-expression tests. After those two are accepted, the remaining relational dialect branches can be rebased onto the same contract.

Generic and MongoDB should remain separate follow-ups: Generic cannot enforce one identifier/expression grammar across arbitrary SQL dialects, while MongoDB needs JavaScript/command-structure-aware validation rather than SQL identifier rules.

This gives us one stable contract to review instead of correcting the same behavior independently across 19 branches.

@openai0229 openai0229 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased onto #2234 and verified the H2 implementation end to end. Identifier quote/remove round trips, conditional versus always-quote behavior, metadata and builder call sites, structural type/default rejection, and legal H2 default semantics are now covered. The real H2 metadata export executes successfully in a second database. Local clean reactor result: 30 H2 tests, 0 failures or errors.

…tterMind#1914)

Override quoteIdentifier/quoteQualifiedIdentifier/buildTableName/buildColumns
and reimplement buildCreateTable/buildAlterTable/buildUpdate/buildTemplate/
buildCreateDatabase in H2SqlBuilder so every identifier is double-quote
escaped and every comment literal is single-quote escaped. Validate
metadata TYPE_NAME against an allow-list and neutralize hostile COLUMN_DEF
defaults in generated DDL. Apply the export SCRIPT NODATA sentinel before
substituting the escaped schema name so schema names containing NODATA are
not corrupted. Document the raw-name contract on H2SqlEscapes and add
attack-string tests for the builder, manager and metadata paths.
…tterMind#1914)

Route column.getColumnType() in buildCreateTable and generateAlterColumnSql
(ADD/MODIFY) through H2SqlEscapes.requireSafeTypeName so a hostile type string
cannot break out of generated DDL. Adds attack-string rejection tests.
… review (OtterMind#1914)

- new H2IdentifierProcessor (SPI ISQLIdentifierProcessor): quoteIdentifier
  with double-quote doubling, escapeString with single-quote doubling
- H2Meta overrides getSQLIdentifierProcessor(); metadata call sites use it
- builders/managers use H2IdentifierProcessor.INSTANCE
- non-escapable validation moved to H2SqlGuards (type names, column defaults)
- H2SqlEscapes removed; tests migrated (27 green)
…h, conditional for SPI, always for DDL) (OtterMind#1914)

The pilot branch predated the dual-track contract; its always-quote
processor had the same null->"" garbage and completion regression the
other modules were fixed for.
@openai0229
openai0229 merged commit 3cb8af5 into OtterMind:main Jul 29, 2026
16 checks passed
@openai0229 openai0229 moved this from In Review to Done in Chat2DB Community Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants